#! /bin/env python
"""pydb.py -- yet another Python debugger!
Interaction prompt line will separate file and call info from code
text using value of line_prefix string.  A newline and arrow may
be to your liking.  You can set it once pydb is imported using the
command "pydb.line_prefix = '\n% '"."""


import string
import regex
import sys
import os
import linecache
import cmdl
import bdb
import repr


# line_prefix = ': '	# Use this to get the old situation back
line_prefix = '\n-> '	# Probably a better default

class Pydb(bdb.Bdb, cmdl.CmdL):
 
	def __init__(self):
		bdb.Bdb.__init__(self)
		cmdl.CmdL.__init__(self)
		self.quit_cmd='quit'
		self.interrupt_cmd=''
		self.ibreak=0
	def exit(self,status=0):
		sys.exit(status)
	def reset(self):
		bdb.Bdb.reset(self)
		self.forget()
	def forget(self):
		self.lineno = None
		self.stack = []
		self.curindex = 0
		self.curframe = None
 
	def setup(self, f, t):
		self.forget()
		self.stack, self.curindex = self.get_stack(f, t)
		self.curframe = self.stack[self.curindex][0]
 
	# Override Bdb methods (except user_call, for now)
 
	def user_line(self, frame):
		"""This function is called when we stop or break at this line."""
		print "user_line",frame==self.botframe
		self.interaction(frame, None)
 
	def user_return(self, frame, return_value):
		"""This function is called when a return trap is set here."""
		frame.f_locals['__return__'] = return_value
		print '--Return--'
		self.interaction(frame, None)
 
	def user_exception(self, frame, (exc_type, exc_value, exc_traceback)):
		"""This function is called if an exception occurs.
		But only if we are to stop at or just below this level."""
		frame.f_locals['__exception__'] = exc_type, exc_value
		if type(exc_type) == type(''):
			exc_type_name = exc_type
		else: exc_type_name = exc_type.__name__
		print exc_type_name + ':', repr.repr(exc_value)
		self.interaction(frame, exc_traceback)

	# General interaction function
 
	def interaction(self, frame, traceback):
		self.setup(frame, traceback)
		self.do_frame(`len(self.stack)-1`)
		self.cmdloop()
		self.forget()

	def do_exec(self, line):
		"""[!|exec] statement
		Execute the (one-line) statement in the context of
		the current stack frame.
		The exclamation point can be omitted unless the first word
		of the statement resembles a debugger command.
		To assign to a global variable you must always prefix the
		command with a 'global' command, e.g.:
			(Pydb) global list_options; list_options = ['-l']
			(Pydb)"""
		if line[:1] == '!': line = line[1:]
		locals = self.curframe.f_locals
		globals = self.curframe.f_globals
		globals['__privileged__'] = 1
		try:
			code = compile(line + '\n', '<stdin>', 'single')
			exec code in globals, locals
		except:
			if type(sys.exc_type) == type(''):
				exc_type_name = sys.exc_type
			else: exc_type_name = sys.exc_type.__name__
			print '***', exc_type_name + ':', sys.exc_value
	default=do_exec

	def fullname(self,name):
		for dirname in sys.path:
			fullname = os.path.join(dirname, name)
			if os.path.exists(fullname):
				return fullname
		#raise RuntimeError, "Can't find file %s"%`name`
		return name
	# Command definitions, called by cmdloop()
	# The argument is the remaining string on the command line
	# Return true to exit from the command loop
	def do_break(self, arg):
		"""b(reak) [ filename:linene | lineno | function ]
		With a line number argument, set a break there in the current
		file.  With a function name, set a break at the entry of that
		function.  Without argument, list all breaks."""
		if not arg:
			for f,l in self.get_all_breaks().items():
				for b in l:
					print "%s:%d" %(self.fullname(f),b) # XXX
			return
		# Try line number as argument
		self.file_line_regex.match(arg)
		try:
			(filename,lineno)=self.split_file_line(arg)
		except:
			try:
				# Try lineno
				lineno = int(eval(arg))
				filename = self.curframe.f_code.co_filename
			except:
				# Try function name as the argument
				import codehack
				try:
					if arg[0]=="'" and arg[-1]=="'":
						# strip off quotes
						arg=arg[1:-1]
					func = eval(arg, self.curframe.f_globals,
							self.curframe.f_locals)
					if hasattr(func, 'im_func'):
						func = func.im_func
					code = func.func_code
				except:
					print '*** Could not eval argument:', arg
					return
				lineno = codehack.getlineno(code)
				filename = code.co_filename

		# now set the break point
		print `filename`,`lineno`
		err = self.set_break(filename, lineno)
		if err: 
			print '***', err
		else:
			self.ibreak=self.ibreak+1
			print "Breakpoint %d at 0: file %s, line %d." \
				%(self.ibreak,filename,lineno)
	do_b=do_break
	def do_clear(self, arg):
		"""cl(ear) [ lineno | filename:lineno ]
		With a line number argument, clear that break in the current file.
		Without argument, clear all breaks (but first ask confirmation)."""
		if not arg:
			# ask to clear all
			try:
				reply = raw_input('Clear all breaks? ')
			except EOFError:
				reply = 'no'
			reply = string.lower(string.strip(reply))
			if reply in ('y', 'yes'):
				self.clear_all_breaks()
			return
		try:
			# is it filename+line?
			(filename,lineno)=self.split_file_line(arg)
		except:
			try:
				# or maybe only the line?
				lineno = int(eval(arg))
			except:
				print '*** Error in argument:', `arg`
				return
			filename = self.curframe.f_code.co_filename
		err = self.clear_break(filename, lineno)
		if err: print '***', err
 
	def do_where(self, arg):
		"""w(here)
		Print a stack trace, with the most recent frame at the bottom.
		An arrow indicates the "current frame", which determines the
		context of most commands."""
		self.print_stack_trace()
	do_w=do_where
	def do_backtrace(self, arg):
		"""backtrace
		Print a stack trace, with the most recent frame at the bottom.
		An arrow indicates the "current frame", which determines the
		context of most commands."""
		i=0
		for frame,lineno in self.stack:
			self.print_backtrace(i,frame,lineno)
			i=i+1

	def do_up(self, arg):
		"""u(p)
		Move the current frame one level up in the stack trace
		(to a newer frame)."""

		if self.curindex == 0:
			print '*** Oldest frame'
		else:
			self.curindex = self.curindex - 1
			self.curframe = self.stack[self.curindex][0]
			self.print_stack_entry(self.stack[self.curindex])
			self.lineno = None
 
	def do_down(self, arg):
		"""d(own)
		Move the current frame one level down in the stack trace
		(to an older frame)."""
		if self.curindex + 1 == len(self.stack):
			print '*** Newest frame'
		else:
			self.curindex = self.curindex + 1
			self.curframe = self.stack[self.curindex][0]
			self.print_stack_entry(self.stack[self.curindex])
			self.lineno = None
 
	def do_frame(self, arg):
		"""f(rame) level
		Move the current frame at level."""
		level=int(eval(arg))
		if level > len(self.stack):
			print '*** Newest frame'
			level=len(self.stack)-1
		elif level <0:
			print '*** Oldest frame'
			level=0
		self.curindex = level
		self.curframe = self.stack[self.curindex][0]
		self.print_stack_entry(self.stack[self.curindex])
		self.lineno = None
	do_f=do_frame
	def do_step(self, arg):
		"""s(tep)
		Execute the current line, stop at the first possible occasion
		(either in a function that is called or in the current function)."""
		self.set_step()
		return 1
	do_s=do_step

	def do_next(self, arg):
		"""n(ext)
		Continue execution until the next line in the current function
		is reached or it returns."""
		self.set_next(self.curframe)
		return 1
 
	def do_return(self, arg):
		"""r(eturn)
		Continue execution until the current function returns."""
		self.set_return(self.curframe)
		return 1
	do_r=do_return
	def do_finish(self, arg):
		"""f(inish)
		Continue execution until the current function returns."""
		self.set_return(self.curframe)
		return 1
 
	def do_continue(self, arg):
		"""c(ontinue)
		Continue execution, only stop when a breakpoint is encountered."""		
		self.set_continue()
		return 1
	do_c = do_continue
 
	def do_quit(self, arg):
		"""q(uit)	Quit from the debugger.
		The program being executed is aborted."""
		self.set_quit()
		return 1
 
	def do_args(self, arg):
		"""a(rgs)
		Print the argument list of the current function."""
		if self.curframe.f_locals.has_key('__args__'):
			print `self.curframe.f_locals['__args__']`
		else:
			print '*** No arguments?!'
 
	def do_retval(self, arg):
		"""retval
		Print the returned object."""
		if self.curframe.f_locals.has_key('__return__'):
			print self.curframe.f_locals['__return__']
		else:
			print '*** Not yet returned!'
 
	def do_print(self, arg):
		"""print expression
		Print the value of the expression."""
		self.curframe.f_globals['__privileged__'] = 1
		try:
			value = eval(arg, self.curframe.f_globals,
							self.curframe.f_locals)
		except:
			if type(sys.exc_type) == type(''):
				exc_type_name = sys.exc_type
			else: exc_type_name = sys.exc_type.__name__
			print '***', exc_type_name + ':', `sys.exc_value`
			return

		print `value`
	do_p=do_print
	def do_list(self, arg):
		"""l(ist) [first [,last]]
		List source code for the current file.
		Without arguments, list 11 lines around the current line
		or continue the previous listing.
		With one argument, list 11 lines starting at that line.
		With two arguments, list the given range;
		if the second argument is less than the first, it is a count."""
		self.lastcmd = 'list'
		last = None
		if arg:
			try:
				x = eval(arg, {}, {})
				if type(x) == type(()):
					first, last = x
					first = int(first)
					last = int(last)
					if last < first:
						# Assume it's a count
						last = first + last
				else:
					first = max(1, int(x) - 5)
			except:
				print '*** Error in argument:', `arg`
				return
		elif self.lineno is None:
			first = max(1, self.curframe.f_lineno - 5)
		else:
			first = self.lineno + 1
		if last == None:
			last = first + 10
		filename = self.curframe.f_code.co_filename
		breaklist = self.get_file_breaks(filename)
		try:
			for lineno in range(first, last+1):
				line = linecache.getline(filename, lineno)
				if not line:
					print '[EOF]'
					break
				else:
					s = string.rjust(`lineno`, 3)
					if len(s) < 4: s = s + ' '
					if lineno in breaklist: s = s + 'B'
					else: s = s + ' '
					if lineno == self.curframe.f_lineno:
						s = s + '->'
					print s + '\t' + line,
					self.lineno = lineno
		except KeyboardInterrupt:
			pass

	def do_whatis(self, arg):
		"""whatis expression
		Prints the type of expression."""
		try:
			value = eval(arg, self.curframe.f_globals, \
					self.curframe.f_locals)
		except:
			if type(sys.exc_type) == type(''):
				exc_type_name = sys.exc_type
			else: exc_type_name = sys.exc_type.__name__
			print '***', exc_type_name + ':', `sys.exc_value`
			return
		code = None
		# Is it a function?
		try: code = value.func_code
		except: pass
		if code:
			print 'Function', code.co_name
			return
		# Is it an instance method?
		try: code = value.im_func.func_code
		except: pass
		if code:
			print 'Method', code.co_name
			return
		# None of the above...
		print type(value)
 
	file_line_regex=regex.symcomp(
			"\(<file>[^:]+\.py\):\(<line>[0-9]+\)"
			)
	def split_file_line(self,arg):
		"""Split argument in and return (filename,lineno).
 
		Raise RuntimeError, if not possible."""
		pat=self.file_line_regex
		if pat.match(arg)<=0:
			raise RuntimeError,'No match'
		lineno = pat.group('line')
		lineno = int(eval(lineno))
		filename = pat.group('file')
		filename = os.path.basename(filename)
		return filename,lineno
 
	# Print a traceback starting at the top stack frame.
	# The most recently entered frame is printed last;
	# this is different from dbx and gdb, but consistent with
	# the Python interpreter's stack trace.
	# It is also consistent with the up/down commands (which are
	# compatible with dbx and gdb: up moves towards 'main()'
	# and down moves towards the most recent stack frame).
 
	def print_backtrace(self,iframe,frame,lineno):
		s=''
		statement=self.format_statement(frame,lineno)
		if statement:
			s=s + ' ' + statement + ' |'
		func=self.format_func(frame)
		if func[0]!='?':
			s=s +' in ' + func
		pos=self.format_pos(frame,lineno)
		if pos[0]!='<':
			s=s +' at ' + pos
		if not s:
			s=' ??'
		print '#%d%s' %( iframe,s)

	def print_stack_trace(self):
		try:
			for frame_lineno in self.stack:
				self.print_stack_entry(frame_lineno)
		except KeyboardInterrupt:
			pass
 
	def format_func(self,frame):
		if frame.f_code.co_name:
			s = frame.f_code.co_name
		else:
			s = "<lambda>"
		if frame.f_locals.has_key('__args__'):
			args = frame.f_locals['__args__']
		else:
			args = None
		if args:
			s = s + repr.repr(args)
		else:
			s = s + '()'
		if frame.f_locals.has_key('__return__'):
			rv = frame.f_locals['__return__']
			s = s + '->'
			s = s + repr.repr(rv)
		return s

	def format_pos(self,frame,lineno):
		filename = frame.f_code.co_filename
		s=os.path.basename(filename)
		return s + ':' + `lineno`

	def format_statement(self,frame,lineno):
		line = linecache.getline(frame.f_code.co_filename, lineno)
		if line: 
			return string.strip(line)
		return ''
 
	def format_stack_entry(self, frame_lineno, lprefix=': '):
		frame, lineno = frame_lineno
		s=''
		filename = self.format_pos(frame, lineno)
		if filename[0]!='<':
			s='\32\32'
		s = s + filename
		s = s + ':\n' + self.format_func(frame)
		#line = linecache.getline(filename, lineno)
		#if line: s = s + lprefix + string.strip(line)
		return s

	def print_stack_entry(self, frame_lineno, prompt_prefix=line_prefix):
		frame, lineno = frame_lineno
		if frame is self.curframe:
			print '>',
		else:
			print ' ',
		print self.format_stack_entry(frame_lineno, prompt_prefix)


	# Help methods (derived from pydb.doc)

	def help_help(self):
		return """h(elp)
		Without argument, print the list of available commands.
		With a command name as argument, print help about that command
		"help pydb" pipes the full documentation file to the $PAGER
		"help exec" gives help on the ! command"""

#	def help_pdb(self):
#		return help()

# Simplified interface

def run(statement, globals=None, locals=None):
	Pydb().run(statement, globals, locals)

def runeval(expression, globals=None, locals=None):
	return Pydb().runeval(expression, globals, locals)

def runctx(statement, globals, locals):
	# B/W compatibility
	run(statement, globals, locals)

def runcall(*args):
	return apply(Pydb().runcall, args)

def set_trace():
	Pydb().set_trace()

# Post-Mortem interface

def post_mortem(t):
	p = Pydb()
	p.reset()
	while t.tb_next <> None: t = t.tb_next
	p.interaction(t.tb_frame, t)

def pm():
	post_mortem(sys.last_traceback)


# Main program for testing

if __name__=='__main__':
	if not sys.argv[1:]:
		print "usage: pydb.py scriptfile [arg] ..."
		sys.exit(2)

	# Get the module name and function name, if present
	filename = sys.argv[1]

	del sys.argv[0]

	run('execfile(' + `filename` + ')', {'__name__': '__main__'})

